/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * sschaffe
 *
 * File Info:
 * User: sschaffe
 * Date: 2011/01/04
 * Time: 10:54
 *
 * Project: KiWi2, kiwi-core module
 */

package kiwi.core.services.transaction;

import kiwi.core.api.persistence.PersistenceService;
import kiwi.core.api.transaction.Transaction;
import kiwi.core.api.transaction.TransactionData;
import kiwi.core.api.transaction.TransactionService;
import kiwi.core.api.triplestore.TripleStore;
import kiwi.core.api.triplestore.TripleStorePersister;
import kiwi.core.exception.transaction.NoSuchTransactionException;
import kiwi.core.exception.transaction.TransactionAlreadyActiveException;
import kiwi.core.exception.transaction.TransactionNotAbortedException;
import kiwi.core.exception.transaction.TransactionNotActiveException;
import kiwi.core.model.rdf.KiWiNamespace;
import kiwi.core.model.rdf.KiWiNode;
import kiwi.core.model.rdf.KiWiTriple;
import kiwi.core.qualifiers.cache.KiWiCache;
import kiwi.core.qualifiers.event.transaction.*;
import kiwi.core.services.triplestore.TripleStorePersisterImpl;
import kiwi.core.services.triplestore.TripleStoreUtil;
import net.sf.ehcache.Cache;
import org.slf4j.Logger;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 * Add file description here!
 * <p/>
 * User: sschaffe
 */
@Named("kiwi.core.transactionService")
@ApplicationScoped
public class TransactionServiceImpl implements TransactionService, Serializable {


    @Inject
    private Logger log;


    @Inject @BeforeCommit
    private Event<TransactionData> beforeCommitEvent;

    @Inject @AfterCommit
    private Event<TransactionData> afterCommitEvent;

    @Inject @BeforeRollback
    private Event<TransactionData> beforeRollbackEvent;

    @Inject @AfterRollback
    private Event<TransactionData> afterRollbackEvent;

    @Inject @OnAbort
    private Event<TransactionData> onAbortEvent;

    /**
     * The Java EE entity manager. Used for persisting and querying triples and nodes in the
     * relational database so that queries can make efficient use of OQL.
     */
    @Inject
    private PersistenceService persistenceService;


    @Inject
    private TripleStore tripleStore;

    // the caches; we remove entries on rollback ...

    @Inject @KiWiCache("triple-cache")
    private Cache tripleCache;

    @Inject @KiWiCache("uri-node-cache")
    private Cache uriNodeCache;

    @Inject @KiWiCache("anon-node-cache")
    private Cache anonNodeCache;

    @Inject @KiWiCache("literal-cache")
    private Cache literalNodeCache;

    @Inject @KiWiCache("namespace-prefix-cache")
    private Cache namespacePrefixCache;

    @Inject @KiWiCache("namespace-uri-cache")
    private Cache namespaceUriCache;


    /**
     * A map to store all currently existing transactions. Used for maintenance purposes.
     */
    private Map<String,Transaction> transactionMap;


    /**
     * Each thread gets its own Transaction for keeping track of all changes happening
     * inside the thread and transaction. We use a ThreadLocal storage to store different instances
     * local to the respective thread.
     */
    private static ThreadLocal<Transaction> instance = new ThreadLocal<Transaction>();


    public TransactionServiceImpl() {
        transactionMap = new HashMap<String,Transaction>();
    }


    /**
     * Clear the transaction from the current thread.
     */
    private static void clearInstance() {
        instance.remove();
    }


    /**
     * Get the transaction with the id given as argument. The method is useful for long-running transactions
     * that span more than one request. In this case, each request must pass on the transaction id to retrieve it.
     * <p/>
     * Each thread may only have a single active transaction.
     * <p/>
     * If no transaction is associated with the thread and the transaction with the given id is already managed
     * by the TransactionService, the transaction is associated with the current thread.
     * <p/>
     * If there is already another transaction associated with the thread, a ConcurrentTransactionException is thrown.
     * <p/>
     * If there is no transaction with the given id, a NoSuchTransactionException is thrown.
     * <p/>
     * TODO: check that it is not associated with a different thread!
     *
     * @param id
     * @return
     */
    @Override
    public Transaction getTransaction(String id) {
        Transaction t = transactionMap.get(id);
        if(t == null) {
            throw new NoSuchTransactionException("Transaction with ID "+id+" does not exist");
        } else if(instance.get() != null && !instance.get().getId().equals(id)) {
            throw new TransactionAlreadyActiveException("Cannot associate transaction "+((Transaction)instance.get()).getId()+" with thread, because transaction "+id+" is already active");
        } else {
            instance.set(t);
        }
        return t;
    }


    /**
      * Get the transaction that is active in the current thread. Each thread may only have a single active transaction.
      * If no transaction is associated, a new transaction is created and associated with the thread.
      * @return
      */
    @Override
    @Produces @Default @Dependent
    public Transaction getTransaction() {
        Transaction t = instance.get();
        if (t == null) {
            t = new Transaction();
            instance.set(t);
            transactionMap.put(t.getId(),t);
            log.info("created new transaction {}", t);
        }

        return t;
    }


    /**
     * Add a transaction dependency to the transaction that is active in the current thread. A transaction dependency
     * means that the current transaction will not commit before all dependencies are committed successfully.
     *
     * @param transactionId
     */
    @Override
    public void addTransactionDependency(String transactionId) {
        Transaction dependency = transactionMap.get(transactionId);
        if(dependency != null) {
            log.debug("adding dependency from transaction {} to transaction {}",getTransaction(),dependency);
            getTransaction().getDependsOn().add(dependency);
        }
    }

    @Override
    public void begin(Transaction t) {
        log.info("starting transaction {}",t.toString());

        if(t.isCreated()) {
            t.setState(Transaction.TransactionState.ACTIVE);
        } else {
            throw new TransactionAlreadyActiveException("The transaction "+t.toString()+" is already active; it should not be started twice");
        }
    }


    /**
     * Commit the transaction passed as argument. Takes the data that is in the transaction and stores it to the
     * database.
     *
      * @param t
     */
    @Override
    public void commit(Transaction t) {
        synchronized (t) {
            try {
                if(t.isActive()) {
                    log.info("Committing transaction {}", t);
                    long start = System.currentTimeMillis();

                    // manage dependencies between transactions
                    for(Transaction dependency : t.getDependsOn()) {
                        synchronized (dependency) {
                            while(dependency.isActive() || dependency.getState() == Transaction.TransactionState.COMMITTING) {
                                try {
                                    log.warn("transaction {} needs to wait for dependency {}",t,dependency);
                                    dependency.wait(1000);
                                    log.debug("dependency resolved; now committing transaction {}",t);
                                } catch (InterruptedException e) { }
                            }
                        }

                        if(dependency.isRolledBack()) {
                            log.error("cannot commit transaction {} because dependency {} has been rolled back",t,dependency);
                            abort(t);
                            rollback(t);
                            return;
                        }
                    }



                    // raise before commit event
                    beforeCommitEvent.fire(t.getData());
                    t.setState(Transaction.TransactionState.COMMITTING);


                    // cleanup transaction data
                    transactionDataCleanup(t.getData());

                    // store transaction data using persister
                    transactionDataPersist(t.getData());


                    // raise after commit event
                    t.setState(Transaction.TransactionState.COMMITTED);
                    afterCommitEvent.fire(t.getData());

                    long end = System.currentTimeMillis();
                    log.info("Committed transaction {} in {}ms ({} triples added, {} triples removed, {} nodes added, {} nodes removed)",
                            new Object[] {t, end-start,
                                    t.getData().getAddedTriples().size(),
                                    t.getData().getRemovedTriples().size(),
                                    t.getData().getAddedNodes().size(),
                                    t.getData().getRemovedNodes().size() });

                    transactionMap.remove(t.getId());
                    clearInstance();

                } else {
                    throw new TransactionNotActiveException("The transaction "+t.toString()+" is not active and cannot be committed");
                }
            } finally {
                t.notifyAll();
            }
        }
    }


    /**
     * Cleanup the transaction data by eliminating nodes, triples and namespaces that have been added and removed
     * subsequently. Note that the order is not important, the method is correct in both cases where an entity
     * has first been added and then removed and vice versa; in both situations, nothing should happen.
     *
     * @param data
     */
    private void transactionDataCleanup(TransactionData data) {


        if(data.getAddedTriples().size() > 0 && data.getRemovedTriples().size() > 0) {
            for(KiWiTriple added : new HashSet<KiWiTriple>(data.getAddedTriples())) {
                if(data.getRemovedTriples().remove(added)) {
                    data.getAddedTriples().remove(added);
                }
            }
        }

        // remove object if it is a literal that is not referred by some other triple
        /*
        for(KiWiTriple triple : data.getRemovedTriples()) {
            if(triple.getObject().isLiteral() && tripleStore.listTriples(null,null,triple.getObject(),null,true,0,1).size() == 0) {
                triple.getObject().setDeleted(true);
                data.getRemovedNodes().add(triple.getObject());
            }
        }
        */

        if(data.getAddedNodes().size() > 0 && data.getRemovedNodes().size() > 0) {
            for(KiWiNode added : new HashSet<KiWiNode>(data.getAddedNodes())) {
                if(data.getRemovedNodes().remove(added)) {
                    data.getAddedNodes().remove(added);
                }
            }
        }


        if(data.getAddedNS().size() > 0 && data.getRemovedNS().size() > 0) {
            for(KiWiNamespace added : new HashSet<KiWiNamespace>(data.getAddedNS())) {
                if(data.getRemovedNS().remove(added)) {
                    data.getAddedNS().remove(added);
                }
            }
        }

    }

    /**
     * Persist the transaction data to the database by starting a new database transaction in the entityManager
     * and then using the tripleStorePersister to store the nodes, triples and namespaes.
     * @param data
     */
    private void transactionDataPersist(TransactionData data) {
        // open a new entity manager transaction
        if(data.getAddedNodes().size() + data.getRemovedNodes().size() +
                data.getAddedNS().size() + data.getRemovedNS().size() +
                data.getAddedTriples().size() + data.getRemovedTriples().size() > 0) {

            EntityManager entityManager = persistenceService.getEntityManager();
            EntityTransaction tx = entityManager.getTransaction();

            try {
                tx.begin();

                TripleStorePersister tripleStorePersister = new TripleStorePersisterImpl(entityManager);
                // store the transaction data in the triple store
                for(KiWiNode n : data.getAddedNodes()) {
                    tripleStorePersister.storeNode(n);
                }

                for(KiWiNode n : data.getRemovedNodes()) {
                    tripleStorePersister.removeNode(n);
                }

                for(KiWiNamespace ns : data.getAddedNS()) {
                    tripleStorePersister.storeNamespace(ns);
                }

                for(KiWiNamespace ns : data.getRemovedNS()) {
                    tripleStorePersister.removeNamespace(ns);
                    removeCachedNamespace(ns);
                }

                tripleStorePersister.storeTriples(data.getAddedTriples());
                tripleStorePersister.removeTriples(data.getRemovedTriples());

                // commit the entity manager transaction
                entityManager.flush();
                tx.commit();

            } catch(RuntimeException ex) {
                log.error("exception while persisting transaction data; transaction data={}",data);
                abort(getTransaction());
                rollback(getTransaction());
                entityManager.getTransaction().rollback();

                throw ex;
            } finally {
                persistenceService.closeEntityManager(entityManager);
            }

        } else {
            log.debug("no transaction data, not opening entity manager transaction ...");
        }

    }


    /**
     * Rollback all changes done to the KiWi system in the transaction passed as argument. Will remove all elements
     * that have been added to the caches and undo all modifications done to triples, resources and namespaces.
     *
     *
     * @param t
     */
    @Override
    public void rollback(Transaction t) {
        synchronized (t) {
            if(t.isAborted()) {
                log.warn("rolling back transaction {}",t);

                // raise before rollback event
                beforeRollbackEvent.fire(t.getData());
                t.setState(Transaction.TransactionState.ROLLINGBACK);

                synchronized (t.getData()) {
                    transactionDataRevertDeleted(t.getData());

                    transactionDataRollbackCaches(t.getData());
                }


                t.setState(Transaction.TransactionState.ROLLEDBACK);
                // raise after rollback event
                afterRollbackEvent.fire(t.getData());

                transactionMap.remove(t.getId());
                clearInstance();

                t.notifyAll();
            } else {
                throw new TransactionNotAbortedException("The transaction "+t.toString()+" is not in state ABORTED and cannot be rolled back");
            }
        }
    }


    /**
     * Undo all deletions performed in the transaction by setting the deleted flags to false again.
     *
     * TODO: at the moment this is unnecessary because the deleted flag is only set by TripleStorePersister
     *
     * @param data
     */
    private void transactionDataRevertDeleted(TransactionData data) {

        for(KiWiTriple t : data.getRemovedTriples()) {
            if(t.isDeleted()) {
                t.setDeleted(false);
            }
        }

        for(KiWiNode n : data.getRemovedNodes()) {
            if(n.isDeleted()) {
                n.setDeleted(false);
            }
        }

        for(KiWiNamespace ns : data.getRemovedNS()) {
            if(ns.getDeleted()) {
                ns.setDeleted(false);
            }
        }

    }

    /**
     * Remove all the data contained in the transaction from the triple store caches.
     *
     * @param data
     */
    private void transactionDataRollbackCaches(TransactionData data) {
         // remove triples from triple cache
        for(KiWiTriple t : data.getAddedTriples()) {
            removeCachedTriple(TripleStoreUtil.createCacheKey(t));
        }

        // remove nodes from the different node caches
        for(KiWiNode n : data.getAddedNodes()) {
            if(n.isLiteral()) {
                literalNodeCache.remove(n.getCacheKey());
            } else if(n.isAnonymousResource()) {
                anonNodeCache.remove(n.getCacheKey());
            } else if(n.isUriResource()) {
                uriNodeCache.remove(n.getCacheKey());
            }
        }

        // remove namespaces frpm namespace caches
        for(KiWiNamespace ns : data.getAddedNS()) {
            removeCachedNamespace(ns);
        }
    }





    private void removeCachedTriple(String key) {
        tripleCache.remove(key);
    }

    private void removeCachedNamespace(KiWiNamespace ns) {
        namespacePrefixCache.remove(ns.getPrefix());
        namespaceUriCache.remove(ns.getUri());
    }





    /**
     * Abort the transaction passed as argument. Changes the state of the transaction to aborted.
     * @param t
     */
    @Override
    public void abort(Transaction t) {
        log.error("aborting transaction {}",t);

        t.setState(Transaction.TransactionState.ABORTED);
        // raise after abort event
        onAbortEvent.fire(t.getData());

    }

    /**
     * Disassociate the current transaction from the current thread; needed for long-running transactions.
     */
    @Override
    public void freeTransaction() {
        clearInstance();
    }
}
